You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
938 lines
33 KiB
938 lines
33 KiB
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler";
|
|
import { useSectionOverlay } from "@/components/Componentes/section-overlay-host";
|
|
import Button from "@/components/Componentes/button";
|
|
import DataErrorState from "@/components/Componentes/data-error-state";
|
|
import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end";
|
|
import { PageLoadingSkeleton } from "@/components/Componentes/page-loading-skeleton";
|
|
import NavigationButton from "@/components/Componentes/navigation-button";
|
|
import { PageBackground } from "@/components/Componentes/page-background";
|
|
import {
|
|
hasQuestionAnswerValue,
|
|
QuestionAnswersProvider,
|
|
useQuestionAnswers,
|
|
} from "@/components/Componentes/question-answer-storage";
|
|
import QuestionExitNavigationButton from "@/components/Componentes/question-exit-navigation-button";
|
|
import QuestionRenderer from "@/components/Componentes/question-renderer";
|
|
import { parseValue as parseBirthplaceValue } from "@/components/Componentes/question-birthplace";
|
|
import QuestionSectionFlow from "@/components/Componentes/question-section-flow";
|
|
import StickyHeader from "@/components/Componentes/sticky-header";
|
|
import TestIntroPage from "@/components/Componentes/test-intro-page";
|
|
import TestQuestionsFlow, {
|
|
type TestQuestion,
|
|
} from "@/components/Componentes/test-questions-flow";
|
|
import {
|
|
getGlasserQuestions,
|
|
useGlasserQuestionsQuery,
|
|
useSubmitGlasserAssessmentMutation,
|
|
} from "@/hooks/marriage/use-glasser";
|
|
import {
|
|
getCattellQuestions,
|
|
useCattellQuestionsQuery,
|
|
useSubmitCattellAssessmentMutation,
|
|
} from "@/hooks/marriage/use-cattell";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { marriageQueryKeys } from "@/hooks/marriage/query-keys";
|
|
import {
|
|
useFormOverviewQuery,
|
|
useFormSectionQuery,
|
|
} from "@/hooks/marriage/use-form-schema";
|
|
import {
|
|
convertOverviewToFrontendItems,
|
|
mapBackendSectionToFrontend,
|
|
type QuestionField,
|
|
} from "@/lib/schema-adapter";
|
|
import { isQuestionVisible, isQuestionRequired } from "@/lib/conditional-rules";
|
|
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
|
|
import { defaultLocale, type Locale } from "@/translations/config";
|
|
import { useI18n } from "@/translations/provider";
|
|
import { useCurrentProfileId } from "@/hooks/use-current-profile-id";
|
|
import {
|
|
getScopedAssessmentDraftKey,
|
|
readScopedAssessmentDraft,
|
|
removeScopedAssessmentDraft,
|
|
} from "@/lib/user-scoped-storage";
|
|
|
|
type QuestionDetailClientProps = {
|
|
closeLabel: string;
|
|
continueLabel: string;
|
|
description: string;
|
|
informationLabel: string;
|
|
itemSlug: string;
|
|
locale?: Locale;
|
|
questionsListHref: string;
|
|
title: string;
|
|
onClose?: () => void;
|
|
};
|
|
|
|
type StoredQuestionField = {
|
|
label?: string;
|
|
value?: unknown;
|
|
type?: string;
|
|
key?: string;
|
|
};
|
|
|
|
type StoredAnswers = {
|
|
fields?: StoredQuestionField[];
|
|
};
|
|
|
|
function getTestDraftStorageKey(slug: string, profileId: number | null) {
|
|
if (!profileId) return null;
|
|
return getScopedAssessmentDraftKey(profileId, slug);
|
|
}
|
|
|
|
function getQuestionStorageKey(slug: string, profileId: number | null) {
|
|
if (!profileId) return null;
|
|
return `marriage:user:${profileId}:sections:${slug}:completed`;
|
|
}
|
|
|
|
function QuestionFlowWrapper({
|
|
questions,
|
|
itemSlug,
|
|
continueLabel,
|
|
questionsListHref,
|
|
onExit,
|
|
}: {
|
|
questions: QuestionField[];
|
|
itemSlug: string;
|
|
continueLabel: string;
|
|
questionsListHref: string;
|
|
onExit?: () => void;
|
|
}) {
|
|
const { getAnswerValue, answers } = useQuestionAnswers();
|
|
const { data: profile } = useMarriageProfileQuery();
|
|
|
|
const computedAge = useMemo(() => {
|
|
if (typeof profile?.age === "number" && !isNaN(profile.age)) {
|
|
return profile.age;
|
|
}
|
|
const dobAnswer =
|
|
answers["personal_identity.date_of_birth"] ||
|
|
answers["personal_info.date_of_birth"] ||
|
|
answers["date_of_birth"] ||
|
|
Object.entries(answers).find(
|
|
([k]) => k.includes("date_of_birth") || k.includes("birth_date"),
|
|
)?.[1];
|
|
|
|
const dobVal =
|
|
typeof dobAnswer === "object" && dobAnswer !== null && "value" in dobAnswer
|
|
? dobAnswer.value
|
|
: dobAnswer;
|
|
|
|
if (dobVal && typeof dobVal === "string") {
|
|
const parts = dobVal.replace(/\//g, "-").split("-");
|
|
if (parts[0] && !isNaN(Number(parts[0]))) {
|
|
const y = Number(parts[0]);
|
|
if (y >= 1300 && y <= 1500) {
|
|
return Math.max(18, 1403 - y);
|
|
} else if (y >= 1900 && y <= 2100) {
|
|
const currentYear = new Date().getFullYear();
|
|
return Math.max(18, currentYear - y);
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}, [profile?.age, answers]);
|
|
|
|
const userContext = useMemo(
|
|
() => ({
|
|
gender: profile?.gender,
|
|
age: computedAge,
|
|
}),
|
|
[profile?.gender, computedAge],
|
|
);
|
|
|
|
|
|
const dynamicQuestions = useMemo(() => {
|
|
return questions
|
|
.filter((q) => isQuestionVisible(q, answers, userContext))
|
|
.map((q) => ({
|
|
...q,
|
|
required: isQuestionRequired(q, answers, userContext),
|
|
}));
|
|
}, [questions, answers, userContext]);
|
|
|
|
const requiredCount = useMemo(
|
|
() => dynamicQuestions.filter((q) => q.required).length,
|
|
[dynamicQuestions],
|
|
);
|
|
|
|
const dobQuestion = useMemo(
|
|
() =>
|
|
dynamicQuestions.find(
|
|
(question) =>
|
|
question.ui_config?.isDob === true || question.type === "date",
|
|
) ||
|
|
questions.find(
|
|
(question) =>
|
|
question.ui_config?.isDob === true || question.type === "date",
|
|
),
|
|
[dynamicQuestions, questions],
|
|
);
|
|
|
|
return (
|
|
<QuestionSectionFlow
|
|
key={itemSlug}
|
|
total={requiredCount}
|
|
continueLabel={continueLabel}
|
|
exitHref={questionsListHref}
|
|
onExit={onExit}
|
|
optionalQuestionIndexes={dynamicQuestions.flatMap((question, index) =>
|
|
question.required ? [] : [index],
|
|
)}
|
|
questions={dynamicQuestions}
|
|
>
|
|
{dynamicQuestions.map((question, index) => {
|
|
const answer = getAnswerValue(question);
|
|
const hasAnswer = hasQuestionAnswerValue(answer ?? null);
|
|
let isAnswered = hasAnswer;
|
|
|
|
if (hasAnswer) {
|
|
const isEmailQuestion =
|
|
question.type === "email" ||
|
|
question.validation?.format === "email";
|
|
if (isEmailQuestion) {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
isAnswered = emailRegex.test(String(answer).trim());
|
|
} else if (question.type === "birthplace") {
|
|
const parsed = parseBirthplaceValue(answer);
|
|
isAnswered = Boolean(parsed.country?.trim() && parsed.city?.trim());
|
|
} else if (question.type === "checkbox") {
|
|
isAnswered = Array.isArray(answer) ? answer.length > 0 : hasAnswer;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={question.id}
|
|
data-question-required={String(question.required)}
|
|
data-question-optional={String(!question.required)}
|
|
data-question-index={index}
|
|
data-question-original-index={question.order}
|
|
data-question-disabled="false"
|
|
data-question-answered={String(isAnswered)}
|
|
>
|
|
<QuestionRenderer question={question} dobQuestion={dobQuestion} />
|
|
</div>
|
|
);
|
|
})}
|
|
</QuestionSectionFlow>
|
|
);
|
|
}
|
|
|
|
export default function QuestionDetailClient({
|
|
closeLabel,
|
|
continueLabel,
|
|
description,
|
|
informationLabel,
|
|
itemSlug,
|
|
locale = defaultLocale,
|
|
questionsListHref,
|
|
title,
|
|
onClose,
|
|
}: QuestionDetailClientProps) {
|
|
const router = useRouter();
|
|
const { dictionary: t } = useI18n();
|
|
const [isTestStarted, setIsTestStarted] = useState(false);
|
|
const [hasTestProgress, setHasTestProgress] = useState(false);
|
|
const queryClient = useQueryClient();
|
|
const profileId = useCurrentProfileId();
|
|
|
|
const handleExit = useCallback(() => {
|
|
if (onClose) {
|
|
onClose();
|
|
return;
|
|
}
|
|
router.replace(questionsListHref);
|
|
}, [onClose, questionsListHref, router]);
|
|
|
|
// Hardware back in the detail page = navigate back to questions list.
|
|
// QuestionAnswersProvider's pagehide/unmount safety net will flush
|
|
// any pending answers automatically when the component unmounts.
|
|
const handleHardwareBack = useCallback(async () => {
|
|
handleExit();
|
|
return true; // handled — keep WebView open
|
|
}, [handleExit]);
|
|
|
|
useHardwareBackHandler(handleHardwareBack);
|
|
|
|
useEffect(() => {
|
|
if (typeof window !== "undefined" && profileId) {
|
|
const draft = readScopedAssessmentDraft(profileId, itemSlug);
|
|
if (
|
|
draft &&
|
|
typeof draft.answers === "object" &&
|
|
draft.answers !== null &&
|
|
Object.keys(draft.answers).length > 0
|
|
) {
|
|
setHasTestProgress(true);
|
|
return;
|
|
}
|
|
setHasTestProgress(false);
|
|
}
|
|
}, [itemSlug, isTestStarted, profileId]);
|
|
|
|
const isCattellSlug =
|
|
itemSlug === "personality_test" || itemSlug === "cattell_test";
|
|
const isGlasserSlug =
|
|
itemSlug === "glasser_5_needs_test" || itemSlug === "glasser_test";
|
|
const isAssessment = isCattellSlug || isGlasserSlug;
|
|
const { data: overview, isLoading: isOverviewLoading, isError: isOverviewError, refetch: refetchOverview } = useFormOverviewQuery(
|
|
"profile",
|
|
locale,
|
|
);
|
|
const { data: sectionResponse, isLoading: isSectionLoading, isError: isSectionError, refetch: refetchSection } =
|
|
useFormSectionQuery(
|
|
"profile",
|
|
itemSlug,
|
|
locale,
|
|
Boolean(itemSlug) && !isAssessment,
|
|
);
|
|
const items = useMemo(
|
|
() => convertOverviewToFrontendItems(overview),
|
|
[overview],
|
|
);
|
|
const overviewItem = items.find((candidate) => candidate.slug === itemSlug);
|
|
const item = useMemo(() => {
|
|
if (!isAssessment && sectionResponse) {
|
|
return mapBackendSectionToFrontend(
|
|
sectionResponse.section,
|
|
sectionResponse.section_progress.completion_percent,
|
|
);
|
|
}
|
|
return overviewItem;
|
|
}, [isAssessment, overviewItem, sectionResponse]);
|
|
const isSchemaLoading = isAssessment
|
|
? isOverviewLoading
|
|
: !sectionResponse && (isOverviewLoading || isSectionLoading);
|
|
const isSchemaError = isAssessment
|
|
? isOverviewError
|
|
: (isOverviewError || isSectionError) && !sectionResponse;
|
|
|
|
const cattellQuery = useCattellQuestionsQuery(locale, {
|
|
enabled: isCattellSlug && isTestStarted,
|
|
retry: 0,
|
|
});
|
|
const submitCattellMutation = useSubmitCattellAssessmentMutation();
|
|
|
|
const glasserQuery = useGlasserQuestionsQuery(locale, {
|
|
enabled: isGlasserSlug && isTestStarted,
|
|
retry: 0,
|
|
});
|
|
const submitGlasserMutation = useSubmitGlasserAssessmentMutation();
|
|
|
|
useEffect(() => {
|
|
if (!isAssessment || isTestStarted) return;
|
|
if (isCattellSlug) {
|
|
void queryClient.prefetchQuery({
|
|
queryKey: marriageQueryKeys.cattellQuestions(locale),
|
|
queryFn: () => getCattellQuestions(locale),
|
|
staleTime: 30 * 1000,
|
|
});
|
|
} else if (isGlasserSlug) {
|
|
void queryClient.prefetchQuery({
|
|
queryKey: marriageQueryKeys.glasserQuestions(locale),
|
|
queryFn: () => getGlasserQuestions(locale),
|
|
staleTime: 30 * 1000,
|
|
});
|
|
}
|
|
}, [
|
|
isAssessment,
|
|
isCattellSlug,
|
|
isGlasserSlug,
|
|
isTestStarted,
|
|
locale,
|
|
queryClient,
|
|
]);
|
|
|
|
const cattellTestQuestions: TestQuestion[] = useMemo(() => {
|
|
const questionsList = cattellQuery.data?.questions || [];
|
|
const OPTION_KEYS = ["A", "B", "C"] as const;
|
|
|
|
return questionsList
|
|
.filter((q: any) => q && (q.question_number || q.id) && q.text)
|
|
.map((q: any) => {
|
|
const rawOptions = Array.isArray(q.options) ? q.options : [];
|
|
const options = rawOptions.map((opt: any, idx: number) => {
|
|
const key = OPTION_KEYS[idx] || String(idx);
|
|
if (typeof opt === "string") {
|
|
return {
|
|
id: key,
|
|
value: key,
|
|
label: opt,
|
|
};
|
|
}
|
|
return {
|
|
id: String(opt.id || opt.value || key),
|
|
value: opt.value ?? key,
|
|
label: String(opt.label || opt.text || opt.title || opt.name || key),
|
|
};
|
|
});
|
|
|
|
return {
|
|
id: Number(q.question_number || q.id),
|
|
text: String(q.text),
|
|
options,
|
|
};
|
|
});
|
|
}, [cattellQuery.data]);
|
|
|
|
const glasserTestQuestions: TestQuestion[] = useMemo(() => {
|
|
const questionsList = glasserQuery.data?.questions || [];
|
|
const DEFAULT_LABELS_FA = ["خیلی کم", "کم", "متوسط", "زیاد", "خیلی زیاد"];
|
|
const DEFAULT_LABELS_EN = [
|
|
"Very Low",
|
|
"Low",
|
|
"Moderate",
|
|
"High",
|
|
"Very High",
|
|
];
|
|
const defaultLabels = locale === "fa" ? DEFAULT_LABELS_FA : DEFAULT_LABELS_EN;
|
|
|
|
return questionsList
|
|
.filter((q: any) => q && (q.question_number || q.id) && q.text)
|
|
.map((q: any) => {
|
|
const rawOptions =
|
|
Array.isArray(q.options) && q.options.length > 0 ? q.options : null;
|
|
const options = rawOptions
|
|
? rawOptions.map((opt: any, idx: number) => {
|
|
const score = idx + 1;
|
|
if (typeof opt === "string") {
|
|
return { id: String(score), value: score, label: opt };
|
|
}
|
|
return {
|
|
id: String(opt.id || opt.value || score),
|
|
value: typeof opt.value === "number" ? opt.value : score,
|
|
label: String(
|
|
opt.label || opt.text || defaultLabels[idx] || String(score),
|
|
),
|
|
};
|
|
})
|
|
: [1, 2, 3, 4, 5].map((score, idx) => ({
|
|
id: String(score),
|
|
value: score,
|
|
label: defaultLabels[idx] || String(score),
|
|
}));
|
|
|
|
return {
|
|
id: Number(q.question_number || q.id),
|
|
text: String(q.text),
|
|
info:
|
|
"factor" in q
|
|
? (q.factor as string)
|
|
: "factor_code" in q
|
|
? (q.factor_code as string)
|
|
: undefined,
|
|
options,
|
|
};
|
|
});
|
|
}, [glasserQuery.data, locale]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
if (!isSchemaLoading && !isSchemaError && !item) {
|
|
handleExit();
|
|
}
|
|
}, [isSchemaLoading, isSchemaError, item, handleExit]);
|
|
|
|
if (isSchemaLoading) {
|
|
if (!isAssessment) {
|
|
const loadingTitle = overviewItem?.title || title;
|
|
|
|
return (
|
|
<>
|
|
<PageBackground disabled />
|
|
<main className="-mx-[17px] flex h-svh flex-col overflow-hidden bg-[#F7F1F0]">
|
|
<StickyHeader
|
|
sticky={false}
|
|
className="question-detail-header shrink-0"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<NavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="close"
|
|
iconLabel={closeLabel}
|
|
onClick={handleExit}
|
|
/>
|
|
<h1 className="min-w-0 flex-1 truncate text-center text-[14px] font-semibold text-white">
|
|
{loadingTitle}
|
|
</h1>
|
|
<NavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="info"
|
|
iconLabel={informationLabel}
|
|
helpTitle={loadingTitle}
|
|
helpDescription={description}
|
|
/>
|
|
</div>
|
|
</StickyHeader>
|
|
|
|
<div className="flex min-h-0 flex-1 items-center justify-center pb-[76px]">
|
|
<span
|
|
role="status"
|
|
className="size-5 animate-spin rounded-full border-2 border-[#E03950]/25 border-t-[#E03950] motion-reduce:animate-none"
|
|
/>
|
|
</div>
|
|
|
|
<FixToTheEnd className="bg-[#F7F1F0]/95">
|
|
<Button disabled>{continueLabel}</Button>
|
|
</FixToTheEnd>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return <PageLoadingSkeleton compact variant="test" />;
|
|
}
|
|
|
|
if (isSchemaError) {
|
|
const errorTitle = overviewItem?.title || title;
|
|
|
|
if (!isAssessment) {
|
|
return (
|
|
<>
|
|
<PageBackground disabled />
|
|
<main className="-mx-[17px] flex h-svh flex-col overflow-hidden bg-[#F7F1F0]">
|
|
<StickyHeader
|
|
sticky={false}
|
|
className="question-detail-header shrink-0"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<NavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="close"
|
|
iconLabel={closeLabel}
|
|
onClick={handleExit}
|
|
/>
|
|
<h1 className="min-w-0 flex-1 truncate text-center text-[14px] font-semibold text-white">
|
|
{errorTitle}
|
|
</h1>
|
|
<NavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="info"
|
|
iconLabel={informationLabel}
|
|
helpTitle={errorTitle}
|
|
helpDescription={description}
|
|
/>
|
|
</div>
|
|
</StickyHeader>
|
|
|
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
<DataErrorState
|
|
onRetry={() => {
|
|
if (isOverviewError) refetchOverview();
|
|
if (isSectionError) refetchSection();
|
|
}}
|
|
/>
|
|
</div>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<PageBackground disabled />
|
|
<main className="-mx-[17px] flex h-svh flex-col overflow-hidden bg-[#F7F1F0]">
|
|
<StickyHeader
|
|
sticky={false}
|
|
className="question-detail-header shrink-0"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<NavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="close"
|
|
iconLabel={closeLabel}
|
|
onClick={handleExit}
|
|
/>
|
|
<h1 className="min-w-0 flex-1 truncate text-center text-[14px] font-semibold text-white">
|
|
{errorTitle}
|
|
</h1>
|
|
<NavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="info"
|
|
iconLabel={informationLabel}
|
|
helpTitle={errorTitle}
|
|
helpDescription={description}
|
|
/>
|
|
</div>
|
|
</StickyHeader>
|
|
|
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
<DataErrorState
|
|
onRetry={() => {
|
|
if (isOverviewError) refetchOverview();
|
|
}}
|
|
/>
|
|
</div>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (!item) {
|
|
return null;
|
|
}
|
|
|
|
if (item && item.questions.length === 0) {
|
|
if (isTestStarted) {
|
|
const isQuestionsLoading = isCattellSlug
|
|
? cattellQuery.isLoading
|
|
: isGlasserSlug
|
|
? glasserQuery.isLoading
|
|
: false;
|
|
|
|
if (isQuestionsLoading) {
|
|
return <PageLoadingSkeleton compact variant="test" />;
|
|
}
|
|
|
|
const activeTestQuestions = isCattellSlug
|
|
? cattellTestQuestions
|
|
: isGlasserSlug
|
|
? glasserTestQuestions
|
|
: [];
|
|
|
|
if (activeTestQuestions.length === 0) {
|
|
const isError = isCattellSlug
|
|
? cattellQuery.isError
|
|
: isGlasserSlug
|
|
? glasserQuery.isError
|
|
: false;
|
|
const refetch = isCattellSlug
|
|
? cattellQuery.refetch
|
|
: glasserQuery.refetch;
|
|
|
|
return (
|
|
<>
|
|
<PageBackground disabled />
|
|
<main className="-mx-[17px] flex h-svh flex-col items-center justify-center gap-4 bg-[#F7F1F0] px-6 text-center">
|
|
<p className="font-semibold text-[#1B1B1B]">
|
|
{isError
|
|
? locale === "fa"
|
|
? "ط®ط·ط§ ط¯ط± ط¯ط±غŒط§ظپطھ ط³ظˆط§ظ„ط§طھ ط§ط² ط³ط±ظˆط±. ظ„ط·ظپط§ظ‹ ط§ط² ط§طھطµط§ظ„ ط§غŒظ†طھط±ظ†طھ غŒط§ ظˆط±ظˆط¯ ط¨ظ‡ طط³ط§ط¨ ع©ط§ط±ط¨ط±غŒ ط§ط·ظ…غŒظ†ط§ظ† طط§طµظ„ ع©ظ†غŒط¯."
|
|
: "Failed to load questions from server. Please check your connection or login status."
|
|
: locale === "fa"
|
|
? "ط³ظˆط§ظ„ط§طھغŒ ط¨ط±ط§غŒ ط§غŒظ† ط¢ط²ظ…ظˆظ† غŒط§ظپطھ ظ†ط´ط¯."
|
|
: "No questions found for this test."}
|
|
</p>
|
|
<div className="flex gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsTestStarted(false)}
|
|
className="rounded-xl bg-[#EFEFEF] px-4 py-2 text-sm font-semibold text-[#1B1B1B]"
|
|
>
|
|
{closeLabel}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => refetch()}
|
|
className="rounded-xl bg-[#F2465F] px-4 py-2 text-sm font-semibold text-white shadow-md"
|
|
>
|
|
{locale === "fa" ? "طھظ„ط§ط´ ظ…ط¬ط¯ط¯" : "Retry"}
|
|
</button>
|
|
</div>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const handleTestFinish = async (
|
|
answers: Record<number, string | number>,
|
|
) => {
|
|
if (isCattellSlug) {
|
|
const responses = Object.entries(answers).map(([qNum, option]) => ({
|
|
question_number: Number(qNum),
|
|
option: String(option),
|
|
}));
|
|
await submitCattellMutation.mutateAsync({ responses });
|
|
try {
|
|
const completionKey = getQuestionStorageKey(item.slug, profileId);
|
|
if (completionKey) {
|
|
window.localStorage.setItem(
|
|
completionKey,
|
|
JSON.stringify({ completed: true }),
|
|
);
|
|
}
|
|
} catch {}
|
|
} else if (isGlasserSlug) {
|
|
const responses = Object.entries(answers).map(([qNum, score]) => ({
|
|
question_number: Number(qNum),
|
|
score: Number(score),
|
|
}));
|
|
await submitGlasserMutation.mutateAsync({ responses });
|
|
try {
|
|
const completionKey = getQuestionStorageKey(item.slug, profileId);
|
|
if (completionKey) {
|
|
window.localStorage.setItem(
|
|
completionKey,
|
|
JSON.stringify({ completed: true }),
|
|
);
|
|
}
|
|
} catch {}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<TestQuestionsFlow
|
|
title={item.title}
|
|
questions={activeTestQuestions}
|
|
closeLabel={closeLabel}
|
|
informationLabel={informationLabel}
|
|
onClose={() => setIsTestStarted(false)}
|
|
onFinish={handleTestFinish}
|
|
draftStorageKey={getTestDraftStorageKey(item.slug, profileId)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const bulletKey =
|
|
item.slug === "glasser_5_needs_test" ? "glasser" : "personality";
|
|
const bullets =
|
|
bulletKey === "glasser"
|
|
? [
|
|
t[
|
|
"Understanding your five basic needs helps you recognize what truly drives your behavior in relationships."
|
|
],
|
|
t[
|
|
"Taking this test is not mandatory, but it will help you better understand your priorities and find a more compatible spouse."
|
|
],
|
|
t[
|
|
"By identifying your strongest needs, you can better communicate your expectations and build healthier relationships."
|
|
],
|
|
]
|
|
: [
|
|
t[
|
|
"Do you struggle to understand the underlying psychological drivers that dictate your interpersonal connections and communication barriers?"
|
|
],
|
|
t[
|
|
"Taking this test is not mandatory, but it will help you better search for a spouse. The Personality test is a test for self-knowledge and better understanding of your spouse."
|
|
],
|
|
t[
|
|
'Do you operate based on superficial behavioral adaptations, or are you aware of the deep "source traits" that fundamentally control your decision-making processes?'
|
|
],
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<PageBackground disabled />
|
|
<main className="-mx-[17px] flex h-svh flex-col overflow-hidden bg-[#F7F1F0]">
|
|
<StickyHeader
|
|
sticky={false}
|
|
className="question-detail-header shrink-0"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<NavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="close"
|
|
iconLabel={closeLabel}
|
|
onClick={handleExit}
|
|
/>
|
|
<h1 className="min-w-0 flex-1 text-center text-[14px] font-semibold text-white truncate">
|
|
{item.title}
|
|
</h1>
|
|
<NavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="info"
|
|
iconLabel={informationLabel}
|
|
helpTitle={item.title}
|
|
helpDescription={description}
|
|
/>
|
|
</div>
|
|
</StickyHeader>
|
|
|
|
<div className="mx-auto flex w-full max-w-md flex-1 flex-col px-[17px] pt-3 min-h-0">
|
|
<TestIntroPage
|
|
title={item.title}
|
|
estimateTime={item.estimate}
|
|
description={t["Estimate time"]}
|
|
bulletPoints={
|
|
isCattellSlug || isGlasserSlug ? undefined : bullets
|
|
}
|
|
disclaimerText={
|
|
t[
|
|
"All provided information is held in strict confidence and will not be disclosed to third parties. This data is utilized exclusively to ensure optimal matchmaking accuracy. Consequently, it is imperative that this assessment be completed with the utmost diligence, precision, and integrity."
|
|
]
|
|
}
|
|
startLabel={hasTestProgress ? t["Continue"] : t["Start"]}
|
|
onStart={() => {
|
|
setIsTestStarted(true);
|
|
}}
|
|
>
|
|
{isCattellSlug ? (
|
|
<div className="mt-5 text-left">
|
|
<ul className="space-y-4">
|
|
{[
|
|
{
|
|
title: t["Understanding Personality Traits"],
|
|
desc: t[
|
|
"Helps provide an overall picture of traits such as sociability, independence, emotional sensitivity, and interpersonal style."
|
|
],
|
|
},
|
|
{
|
|
title: t["Assessing Communication Style"],
|
|
desc: t[
|
|
"Shows how a person typically communicates, expresses emotions, and builds closeness in relationships."
|
|
],
|
|
},
|
|
{
|
|
title:
|
|
t["Understanding Responses to Stress and Conflict"],
|
|
desc: t[
|
|
"Offers insight into emotional stability, tension levels, and how a person may react in difficult or stressful situations."
|
|
],
|
|
},
|
|
{
|
|
title: t["Assessing Independence and Decision-Making"],
|
|
desc: t[
|
|
"Helps identify the person’s level of independence, assertiveness, and preference for individual or shared decision-making."
|
|
],
|
|
},
|
|
{
|
|
title:
|
|
t["Identifying Potential Differences and Challenges"],
|
|
desc: t[
|
|
"Comparing two individuals’ results can highlight personality differences that may require attention in a long-term relationship."
|
|
],
|
|
},
|
|
{
|
|
title: t["Supporting Better Match Recommendations"],
|
|
desc: t[
|
|
"Alongside interviews and other relationship criteria, personality assessment can help make the matching process more targeted and improve the evaluation of compatibility."
|
|
],
|
|
},
|
|
].map((bullet, index) => (
|
|
<li key={index} className="min-w-0">
|
|
<h4 className="group-12 font-bold text-[#1B1B1B] leading-snug">
|
|
{bullet.title}
|
|
</h4>
|
|
<p className="group-12 text-[#5A5A5A] mt-1 leading-[1.55] text-justify">
|
|
{bullet.desc}
|
|
</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
) : isGlasserSlug ? (
|
|
<div className="mt-5 text-left">
|
|
<ul className="space-y-4">
|
|
{[
|
|
{
|
|
title: t["Understanding Core Psychological Needs"],
|
|
desc: t[
|
|
"Helps identify the importance of the five basic needs—love and belonging, power, freedom, fun, and survival—in each person’s life."
|
|
],
|
|
},
|
|
{
|
|
title: t["Recognizing Relationship Expectations"],
|
|
desc: t[
|
|
"Provides insight into what each person expects from a relationship, such as closeness, independence, security, achievement, or shared enjoyment."
|
|
],
|
|
},
|
|
{
|
|
title: t["Assessing Personality Compatibility"],
|
|
desc: t[
|
|
"Helps compare personality traits, behavioral tendencies, and interaction styles to identify areas of compatibility between two individuals."
|
|
],
|
|
},
|
|
{
|
|
title: t["Identifying Potential Sources of Conflict"],
|
|
desc: t[
|
|
"Differences in needs or personality styles can highlight areas where misunderstandings, tension, or disagreements may arise in the relationship."
|
|
],
|
|
},
|
|
{
|
|
title: t["Improving Mutual Understanding"],
|
|
desc: t[
|
|
"Helps individuals better understand their own needs as well as their partner’s motivations, preferences, and emotional priorities."
|
|
],
|
|
},
|
|
{
|
|
title:
|
|
t["Supporting More Suitable Match Recommendations"],
|
|
desc: t[
|
|
"Combining needs and personality assessments with interviews and other marriage criteria can help make partner recommendations more personalized and well-matched."
|
|
],
|
|
},
|
|
].map((bullet, index) => (
|
|
<li key={index} className="min-w-0">
|
|
<h4 className="group-12 font-bold text-[#1B1B1B] leading-snug">
|
|
{bullet.title}
|
|
</h4>
|
|
<p className="group-12 text-[#5A5A5A] mt-1 leading-[1.55] text-justify">
|
|
{bullet.desc}
|
|
</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
) : null}
|
|
</TestIntroPage>
|
|
</div>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<PageBackground disabled />
|
|
|
|
<QuestionAnswersProvider
|
|
slug={item.slug}
|
|
questions={item.questions}
|
|
locale={locale}
|
|
>
|
|
<main className="-mx-[17px] flex h-svh flex-col overflow-hidden bg-[#F7F1F0]">
|
|
<StickyHeader
|
|
sticky={false}
|
|
className="question-detail-header shrink-0"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<QuestionExitNavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="close"
|
|
iconLabel={closeLabel}
|
|
exitHref={questionsListHref}
|
|
onExit={handleExit}
|
|
/>
|
|
<h1 className="min-w-0 flex-1 text-center text-[14px] font-semibold text-white truncate">
|
|
{item.title}
|
|
</h1>
|
|
<NavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="info"
|
|
iconLabel={informationLabel}
|
|
helpTitle={item.title}
|
|
helpDescription={description}
|
|
/>
|
|
</div>
|
|
</StickyHeader>
|
|
|
|
<div className="mx-auto flex w-full max-w-md flex-1 flex-col px-[17px] pt-3 min-h-0">
|
|
<QuestionFlowWrapper
|
|
questions={item.questions}
|
|
itemSlug={item.slug}
|
|
continueLabel={continueLabel}
|
|
questionsListHref={questionsListHref}
|
|
onExit={handleExit}
|
|
/>
|
|
</div>
|
|
</main>
|
|
</QuestionAnswersProvider>
|
|
</>
|
|
);
|
|
}
|